﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;

namespace XOS_VHD_FAT32
{
    class FileStream : Stream
    {
        private DirEntry _file;
        private readonly FAT32Driver _drv;

        public FileStream(DirEntry file, FAT32Driver drv)
        {
            _file = file;
            _drv = drv;

            Trace.WriteLine("File: " + file.Name + "; Size: " + file.Size + " bytes");
            Trace.WriteLine("  FAT chain: ");
            Trace.WriteLine("        ClustNo    ClustId  NextClustId");

            
            var c = _file.FirstCluster;
            var chain = new List<uint>(Convert.ToInt32(Math.Ceiling(file.Size / (Convert.ToDouble(drv._bytesPerClust)))) + 1);
            var i = 0;
            while (c !=0xffffffff)
            {
                chain.Add(c);

                var cc = _drv.GetNextClusterID(c);
                Trace.WriteLine("        " + i.ToString("X8") + "   " + c.ToString("X8") + " " + cc.ToString("X8"));

                c = cc;

                i++;
            }

            _clustNo = 0;

            _clusts = chain.ToArray();

            _buff = _drv.ReadCluster(_file.FirstCluster);
        }

        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return true; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }

        public override void Flush()
        {
            _drv.FlushFAT();
        }

        public override long Length
        {
            get { return _file.Size; }
        }

        private long _pos;
        public override long Position
        {
            get { return _pos; }
            set
            {
                if (value > Length)
                {
                    _pos = Length;
                }
                else if (value < 0)
                {
                    _pos = 0;
                }
                else
                {
                    _pos = value;
                }
            }
        }

        private int _clustNo = 0;
        private byte[] _buff;
        private uint[] _clusts;


        public override int Read(byte[] buffer, int offset, int count)
        {
            if (Position >= Length)
            {
                return -1;
            }

            var _curClust = _clusts[_clustNo];

            var clustSz = _drv._sectPerClust * _drv._bytesPerSect;
            var beg = clustSz * _clustNo;
            var eoc = beg + clustSz;
            var end = Math.Min(_file.Size, eoc);

            var bytesCount = 0;

        DoRead:
            if (Position >= beg && Position + count <= end)
            {
                // all the data is cached (at least partly)

                if (Position + count > end)
                {
                    // read first part of data
                    var newCnt = end - Position - count;

                    if (newCnt > 0)
                    {
                        Array.Copy(_buff, Position - beg, buffer, offset, newCnt);

                        offset += (int)newCnt;
                        count -= (int)newCnt;

                        Position += (int)newCnt;

                        bytesCount += (int)newCnt;
                    }

                    if (end == eoc)
                    {
                        // not a eof, just end of cluster
                        goto DoRead;
                    }
                }
                else
                {
                    // read all data
                    Array.Copy(_buff, Position - beg, buffer, offset, count);

                    Position += count;

                    bytesCount = count;
                }
            }
            else
            {
                // we must read new cluster and try to do some reading
                _curClust = _drv.GetNextClusterID(_curClust);

                if (_curClust < 0x0ffffff)
                {
                    // Yes, we have some more clusters - let's read
                    _buff = _drv.ReadCluster(_curClust);

                    goto DoRead;
                }
            }

            return bytesCount;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            switch (origin)
            {
                case SeekOrigin.Begin:
                    Position = offset;
                    break;
                case SeekOrigin.Current:
                    Position += offset;
                    break;
                case SeekOrigin.End:
                    Position = Length + offset;
                    break;
            }

            return Position;
        }

        public override void SetLength(long value)
        {
            var bytesPerClust = Convert.ToDouble(_drv._bytesPerSect * _drv._sectPerClust);

            var curClustCnt = Convert.ToInt64(Math.Ceiling(Length / bytesPerClust));
            var newClustCnt = Convert.ToInt64(Math.Ceiling(value / bytesPerClust));

            if (newClustCnt < curClustCnt)
            {
                // trunc
                // Just removing some unnessesary clusters from FAT chain

                var cc = _file.FirstCluster;

                for (int i = 0; i < newClustCnt; i++)
                {
                    cc = _drv._fatChain[(int)cc];
                }

                // cc - first clust to remove
                var lastClust = cc;
                while (cc < 0x0fffffff)
                {
                    var c = _drv._fatChain[cc];

                    _drv._fatChain[cc] = 0;

                    cc = c;
                }

                _drv._fatChain[lastClust] = 0xffffffff;

                _file.Size = (uint)value;
            }
            else if (newClustCnt > curClustCnt)
            {
                // enlarging
                // adding some clusters to FAT chain
                var cc = _file.FirstCluster;

                while (_drv._fatChain[cc] < 0x0fffffff)
                {
                    cc = _drv._fatChain[cc];
                }

                var clustToAdd = newClustCnt - curClustCnt;
                var searchingForTerm = false;
                var zeroBuff = new byte[_drv._bytesPerClust];

                for (uint i = 0; i < _drv._fatChain.Length; i++)
                {
                    if (_drv._fatChain[i] == 0)
                    {
                        if (searchingForTerm)
                        {
                            _drv._fatChain[cc] = 0xffffffff;
                            break;
                        }

                        // item is free - let's use it

                        _drv.WriteCluster(_drv._fatChain[i], zeroBuff);

                        _drv._fatChain[cc] = i;
                        cc = i;

                        clustToAdd--;

                        if (clustToAdd == 0)
                        {
                            searchingForTerm = true;
                        }
                    }
                }

                if (clustToAdd != 0)
                {
                    throw new InvalidOperationException();
                }

                _file.Size = (uint)value;
            }
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
            {
                throw new ArgumentNullException("buffer");
            }

            if (buffer.Length < offset)
            {
                throw new ArgumentOutOfRangeException("offset");
            }

            if (buffer.Length < offset + count)
            {
                throw new ArgumentOutOfRangeException("count");
            }

            if (Position + count > Length)
            {
                SetLength(Position + count);
            }


            while (count > 0)
            {
                var clustId = (Position / _drv._bytesPerClust);
                var inclustOffs = Position % _drv._bytesPerClust;

                var firstSectToMod = inclustOffs / _drv._bytesPerSect;

                var sectId = _drv.GetLBAForCluster(_clusts[clustId]) + firstSectToMod;

                var bytes = _drv.Partition.ReadSectors(sectId, 1);

                var insectOffs = inclustOffs % _drv._bytesPerSect;

                var bytesToWrite = (int)Math.Min(count, _drv._bytesPerSect - insectOffs);

                Array.Copy(buffer, offset, bytes, insectOffs, bytesToWrite);

                _drv.Partition.WriteSectors(bytes, sectId, 1);

                count -= bytesToWrite;
            }
        }
    }

}
